package cli

import (
	"bytes"
	"github.com/kataras/golog"
	"regexp"
	"sdn-executor/core/ssh"
	model "sdn-executor/model/cli"
	"sdn-executor/model/common"
	"sdn-executor/storage/sql/executor"
	"sdn-executor/utils/erros"
	"strings"
	"time"
)

const (
	DEFAULT_PROMPT        = "[>\\]#$]"
	DEFAULT_STRICT_PROMPT = "[>\\]#]$"
	LINUX_START_PROMPT    = "@"
	LINUX_END_PROMPT      = "~"
	MODEL_START_PROMPT    = "("
	MODEL_END_PROMPT      = ")"
	ROLE_START_PROMPT     = "["
	LOGIN_PROMPT          = "([Ll]ogin|Username):"
	PWD_PROMPT            = "[Pp]assword:"
	ERROR_PROMPT          = "('[\"^\"]')," +
		"do not have permission|Command not allowed|" +
		"[Aa]uthorization failed|[Pp]ermission [Dd]enied|[Nn]o [Pp]ermission," +
		"(\nError),(\nerror),(syntax error),(unknown command),(Unknown command)"
	END_PROMPT = "Y/N,y/n,Yes/No,Input password:,su[\\s\\S]+(Password:)," +
		"BCM.0\\s*>,ftp>,yes/no,-+[ ]*[\\(]*[ ]*[Mm]ore[ ]*[\\d]*[%]*[ ]*[\\)]*[ ]*-+"
	REBOOT_PROMPT = "(closed), (Closing), (Connection), (rebooting)"
	MORE_PROMPT   = "-+[ ]*[\\(]*[ ]*[Mm]ore[ ]*[\\d]*[%]*[ ]*[\\)]*[ ]*-+"
	NO_MORE_FLAG  = "no-more"

	DEFAULT_PORT            = 22
	DEFAULT_CONN_TIMEOUT    = 5 * time.Second
	DEFAULT_READ_TIMEOUT    = 10 * time.Second
	DEFAULT_WRITE_TIMEOUT   = 10 * time.Second
	DEFAULT_REQUEST_TIMEOUT = 180 * time.Second
)

var (
	networkPrompts = [...]string{">", "#", "]", "<", "[", "$"}
	vendors        = [...]string{"Ruijie", "Huawei", "H3C", "Brocade", "Foundry", "Huarong", "Mellanox", "Unis"}
)

type Client struct {
	Device    string
	session   *ssh.Session
	SysPrompt string
}

// NewClient 创建客户端并解析提示符
func NewClient(device string, port int16, user string, pwd string) (*Client, *erros.Error) {
	golog.Infof("device: %s, port: %d, user: %s, pwd: %s", device, port, user, pwd)
	sshClient, err := ssh.NewSimpleClient(device, port, user, pwd)
	if err != nil {
		golog.Errorf("NewSimpleClient failed. device: %s", device)
		return nil, err
	}
	session, err := sshClient.NewSession(ssh.SubsystemShell)
	if err != nil {
		golog.Errorf("NewSession failed. device: %s", device)
		sshClient.Close()
		return nil, err
	}

	result, _, err := session.RcvUntilRegexps([]string{DEFAULT_PROMPT}, 20*time.Second, true, true)
	if err != nil {
		golog.Errorf("RcvUntilRegexps failed. device: %s", device)
		return nil, erros.New(erros.CodeInternalError, "RcvUntilRegexps failed")
	}
	sysPromt := extractHostname(result)
	golog.Infof("extractHostname: %s, result: %s", sysPromt, string(result))

	return &Client{
		Device:    sshClient.Device,
		session:   session,
		SysPrompt: sysPromt,
	}, nil
}

// SendCommands 一次放批量命令
func (c *Client) SendCommands(
	query *model.Query,
	requestId string,
	requestType string,
) ([]*model.CommandResult, *erros.Error) {

	// 预处理
	cmds := query.Cmds
	cmdMap, firstIndex := c.normCmds(cmds)

	// 执行命令
	results := make([]*model.CommandResult, 0)
	index := firstIndex
	order := 1
	for cmd, ok := cmdMap[index]; ok; order++ {
		createSdnExecutorResult(cmd.Cmd, order, &query.BaseQuery, requestId, requestType)

		echo, offset, err := c.SendCommand(cmd)
		cmdResult := &model.CommandResult{
			Order: order,
			Cmd:   cmd.Cmd,
			Echo:  string(echo),
		}
		results = append(results, cmdResult)
		if err != nil {
			cmdResult.Status = common.ExecStatusFail
			executor.FailSdnExecutorResult(requestId, order, string(echo), err.Msg)
			return results, err
		}
		for _, errRegex := range cmd.ErrorRegexps {
			regexp, e := regexp.Compile(errRegex)
			if e != nil {
				cmdResult.Status = common.ExecStatusFail
				executor.FailSdnExecutorResult(requestId, order, string(echo), err.Msg)
				return results, err
			}
			matched := regexp.Match(echo)
			if matched {
				cmdResult.Status = common.ExecStatusFail
				executor.FailSdnExecutorResult(requestId, order, string(echo), "match errRegex")
				return results, err
			}
		}

		cmdResult.Status = common.ExecStatusSuccess
		executor.SuccessSdnExecutorResult(requestId, order, string(echo))

		// 取下一个待执行的命令
		index += offset
		cmd, ok = cmdMap[index]
	}

	return results, nil
}

// SendCommand 发送一条命令
func (c *Client) SendCommand(cmd *model.Command) ([]byte, int, *erros.Error) {

	// 提示符
	prompts := make([]string, 0, len(cmd.NextOffset))
	for prompt := range cmd.NextOffset {
		prompts = append(prompts, prompt)
	}

	golog.Info("SendCommand: %s", cmd.Cmd)
	echo, idx, err := c.SendAndRcv(cmd.Cmd, prompts, cmd.CmdTimeout, true, true)
	if err != nil {
		golog.Errorf("SendAndReceive failed, cmd: %s, devcie: %s", cmd.Cmd, c.Device)
		return echo, -1, err
	}

	return echo, cmd.NextOffset[prompts[idx]], err
}

func (c *Client) SendAndRcv(
	cmd string,
	prompts []string,
	requestTimeout time.Duration,
	oneLine bool,
	matchMoreLine bool,
) ([]byte, int, *erros.Error) {

	// 添加换行符
	request := cmd
	if !strings.HasSuffix(cmd, "\n") {
		request += "\n"
	}

	// 发送命令
	_, err := c.session.Send(request)
	if err != nil {
		golog.Errorf("Send failed. request: %s, device: %s, err: %+v", request, c.Device, err)
		return nil, -1, err
	}

	// 结果
	return c.session.RcvUntilRegexps(prompts, requestTimeout, oneLine, matchMoreLine)
}

func (c *Client) normCmds(cmds []*model.Command) (map[int]*model.Command, int) {
	cmdMap := make(map[int]*model.Command)
	firstIndex := 0
	for _, v := range cmds {
		cmdMap[v.Index] = v
		if v.Index < firstIndex {
			firstIndex = v.Index
		}
		// 归一化offset
		if len(v.NextOffset) <= 0 {
			nextOffset := make(map[string]int)
			nextOffset[c.SysPrompt] = 1
			v.NextOffset = nextOffset
		} else {
			if offset, ok := v.NextOffset["system"]; ok {
				v.NextOffset[c.SysPrompt] = offset
				delete(v.NextOffset, "system")
			}
		}
	}

	return cmdMap, firstIndex
}

func (c *Client) Close() {
	if c != nil && c.session != nil {
		if c.session.Client != nil {
			c.session.Client.Close()
		}
		c.session.Close()
	}
}

func extractHostname(echo []byte) string {
	lastNewLinePos := bytes.LastIndexByte(echo, '\n')
	lastLine := echo[lastNewLinePos+1:]
	buf := lastLine[:]

	startPos := bytes.Index(lastLine, []byte(LINUX_START_PROMPT))
	if startPos != -1 {
		endPos := bytes.LastIndex(lastLine, []byte(LINUX_END_PROMPT))
		if endPos != -1 {
			buf = lastLine[startPos+1 : endPos]
		} else {
			buf = lastLine[startPos+1:]
		}
	} else {
		startPos = bytes.Index(lastLine, []byte(MODEL_START_PROMPT))
		endPos := bytes.LastIndex(lastLine, []byte(MODEL_END_PROMPT))
		if startPos != -1 && endPos != -1 {
			buf = lastLine[startPos+1:]
		}

		pos := bytes.LastIndex(buf, []byte(ROLE_START_PROMPT))
		if pos != -1 {
			buf = buf[:pos]
		}
	}

	hostname := string(buf)
	for _, v := range networkPrompts {
		hostname = strings.ReplaceAll(hostname, v, "")
	}
	hostname = strings.TrimSpace(hostname)
	hostname = strings.ReplaceAll(hostname, "\\?7h", "")

	return hostname
}

func createSdnExecutorResult(cmd string, sequence int, baseQuery *common.BaseQuery, requestId string, requestType string) *erros.Error {
	dbResult := executor.SdnExecutorResult{
		Device:      baseQuery.Device,
		Status:      string(common.ExecStatusExecuting),
		Sequence:    sequence,
		RequestId:   requestId,
		RequestType: requestType,
		Channel:     baseQuery.Channel,
		Priority:    baseQuery.Priority,
		User:        baseQuery.User,
		Caller:      baseQuery.Caller,
		ReadOnly:    baseQuery.ReadOnly,
		Request:     cmd,
	}

	return executor.CreateSdnExecutorResult(&dbResult)
}
